Skip to content

refactor(UI): add scoped scroll restoration and Duo layout groundwork - #378

Open
kyleve wants to merge 11 commits into
mainfrom
codex/duo-navigation-foundation
Open

kyleve wants to merge 11 commits into
mainfrom
codex/duo-navigation-foundation

Conversation

@kyleve

@kyleve kyleve commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner

Posted by an AI agent on kve's behalf.

Summary

Add scoped scroll restoration and shared Duo pose fixtures for adaptive UI. Stable views own navigation, presentation, and explicit reveal requests. This PR supplies groundwork; #372 integrates it into Where.

Ownership and APIs

API Responsibility
SnapshotConfiguration.DuoPose Seven synthetic poses, localized labels, existing frames/traits, and typed reserved-region geometry with activation state.
BScrollRestorationStore and .bScrollRestorationScope() Retain entries by view type and typed role within an explicit stable scene/session scope. Context changes replace an entry rather than retaining historical contexts.
BScrollPosition<ID, Context> Supply a native scroll binding, semantic commands, retained alignment, and initialization after content loads. Keep context typed until the heterogeneous store boundary.
AdaptivePaneGeometry and AdaptivePaneArrangement Map physical container geometry and resolve compact, columns, or tabletop using stylesheet limits.

The store retains semantic content identity and alignment, without pixel restoration, persistence, application navigation, or pane synchronization. Without a scope, positions remain view-local. Consumers choose readiness, initial positioning, validity, and fallback behavior.

A pending initialization cannot replace a later user position. Old bindings and outgoing views retain detached entries after context replacement. Reading an unmounted wrapper asserts in debug; release access uses an isolated attachment that cannot claim a scoped entry.

Where has no shared selection coordinator. #372 keeps a small month-reveal value in Locations and gives Timeline its own row IDs. RegionStint.ID is a typed Hashable/Sendable region/start-date identity; this source API change does not alter persisted data.

AdaptivePaneGeometryReader also supports the welcome and developer overlays. It does not implement navigation bars or toolbars. Native containers retain that responsibility. Broadway supplies column-card styling, and the architecture rule confines system reserved-region reads to the geometry adapter.

Duo fixtures retain their frame names, dimensions, traits, snapshot identifiers, and measurement provenance. SnapshotKit owns the labels; WhereUI adapts its reserved-region values to production geometry.

Composition

@BScrollPosition<CalendarPresentationState.Target, ScrollContext>
private var detailPosition: ScrollPosition

// In the view initializer:
_detailPosition = BScrollPosition(
    Self.self,
    role: scrollRole,
    context: { ScrollContext(region: focusedRegion, year: report.selectedYear) },
    idType: CalendarPresentationState.Target.self,
)

The consumer in #372 places .bScrollRestorationScope() on a stable ancestor and binds the scroll view to $detailPosition. Once content is ready, initialize(id:anchor:) restores the saved target or applies that view's initial policy.

Review focus

  • BScrollPosition and its mounted tests: replacement, typed contexts, independent roles/scopes, stale bindings, and delayed readiness.
  • DuoPose: shared labels and unchanged geometry/identifiers.
  • AdaptivePaneGeometry: physical reserved-region coordinates, asymmetric insets, and RTL. The policy tests cover minimum readable pane sizes.
  • Independent review found no actionable issue in the final foundation or affected consumer contracts. Release fallback isolation was source-inspected, not executed by debug tests.

Testing

  • Focused foundation validation passed 12 tests: mounted BScrollPosition, SnapshotDuoPose, and AdaptivePaneContainer.
  • ./test BroadwayUITests SnapshotKitTests WhereUITests passed all 29 BroadwayUI and 58 SnapshotKit tests. Its 711 WhereUI tests found four calendar binding-observation failures and one new timeline test waiting on a transient command; all five were corrected and the rebuilt 19-test mounted regression set passed.
  • The affected seven screen snapshot suites passed across the broad run and focused final revalidation, using --record never --review. This review follow-up updates five reviewed accessibility legends in feat(WhereUI): adapt Where for iPhone Duo #372 and removes one obsolete linked-timeline capture; screen pixels in those five updated captures are unchanged.
  • Architecture checks passed 29 tests. Default host checks, ./swiftformat --lint, ./xcstrings --lint, and complete PR diff checks passed.
  • Mounted tests cover replacement, delayed alignment, context reset, stale callbacks, independent roles/scopes, and operation without a scope. Release-only fallback behavior was source-inspected; real touch gestures remain a separate runtime limit.

Native Duo 27.2 geometry measurement remains pending. Synthetic layouts and hosted state tests do not establish native folding, system-bar placement, keyboard behavior, or spoken VoiceOver behavior.

Stack

First active layer, based on main after #377 merged. #372 adds consumer integration. Both PRs remain open for review.

Backlog reconciliation

The 13 preserved Duo records are tracked in Linear. STU-318 owns adoption and its six implementation sub-issues. This slice relates to STU-321, STU-324. Unmerged implementation remains In Review.

Remaining Backlog work: native Duo release validation (STU-316), intermittent Locations settling diagnostics (STU-317), and tall Year accessibility annotations (STU-325). Existing STU-114 and STU-252 now distinguish the partial fixes from their remaining requirements; both retain their priorities, labels, and open state.

@kyleve
kyleve added this pull request to stack #379 October 16, 2026 14:07
@kyleve kyleve changed the title Add adaptive history navigation and layout foundations refactor(WhereUI): add adaptive history state and layout policy Oct 16, 2026
@kyleve
kyleve force-pushed the codex/duo-navigation-foundation branch 2 times, most recently from 740f7b9 to d78b76d Compare October 31, 2026 00:50
@kyleve
kyleve force-pushed the codex/duo-navigation-foundation branch from d78b76d to d27a835 Compare November 9, 2026 22:30
@kyleve
kyleve force-pushed the codex/duo-navigation-foundation branch from d27a835 to 6a04f77 Compare November 11, 2026 17:04
kyleve added a commit that referenced this pull request Nov 11, 2026
> _Posted by an AI agent on kve's behalf._

## Summary

Add typed horizontal and vertical size classes to Broadway. UIKit roots
observe changes live; SwiftUI roots map their environment into the same
traits. Scoped overrides let component styles resolve the container's
layout policy.

This is foundation only. It preserves the current main toolchain and
screen behavior.

## Primary APIs

`BHorizontalSizeClass` and `BVerticalSizeClass` expose only compact and
regular values. Both default to compact. UIKit unspecified/unknown
values and SwiftUI missing/unknown values normalize to compact at the
bridge, so stylesheet switches need only two cases. `BTraits.system`
registers both, and `bHorizontalSizeClass` / `bVerticalSizeClass`
provide scoped SwiftUI overrides.

```swift
content
    .bHorizontalSizeClass(.regular)
    .bVerticalSizeClass(.regular)
```

Stylesheets read `context.traits.horizontalSizeClass` and
`verticalSizeClass` during slicing. Measured geometry remains a view
input. The fallback is an explicit styling policy; known system traits
and scoped overrides are preserved without inferring size classes from
device identity or bounds.

Independent review found one downstream test matrix using the removed
enum case. PR #378 now tests all nine raw UIKit size-class combinations
through the Broadway bridge, preserving single-pane fallback coverage.

## Stack

1 of 5, based on `main`. The snapshot foundation follows this PR;
reusable navigation and geometry come next. [The final app
PR](#372) describes the complete
experience and remaining native release checks.

## Testing

- At current stack tip `2fd6f699853ac4fa592a90d3928f83e836bca635`,
`./test --all` passed on Xcode 27.2: 2,975 iOS unit tests, 28 host macro
tests, and 31 architecture tests. SwiftFormat passed. The root PR also
passed 141 Broadway tests from an isolated build; an earlier
shared-cache build hit stale LifecycleKit deployment-target metadata.
- The earlier full integration at
`e9236bb54771a1e144ffbc35ea9076e8f2c4569f` passed both CI gates,
including all image suites. The current change normalizes unavailable
Broadway size classes to compact and updates the downstream UIKit-input
regression matrix; reference PNGs and renderer settings are unchanged.
New full gates are running on the current tip: [GitHub
Actions](https://github.com/kyleve/Stuff/actions/runs/43064578603) and
[CircleCI pipeline
2166](https://app.circleci.com/pipelines/circleci/HfkN3cJ2hk23YfG42g3oS1/E7hn5FALiSX67sknpk9SZu/2166).
- Visual review covers every changed reference, including full-height
content, accessibility annotations, large text, and theme panels.
Reference PNG metadata and comparison tolerances are preserved.
- Independent review covered all five stack slices and the final
capture, navigation, and inset changes. Focused mutation tests
demonstrate that stale-inset delivery and off-screen accessibility
regressions fail without their fixes.
- Two tall Year accessibility images retain a pre-existing marker offset
and incomplete native-toolbar legends. The capture follow-up is
documented below; it is tracked in
[STU-325](https://linear.app/kve/issue/STU-325); these images do not
prove live VoiceOver behavior.
- Native Duo 27.2 geometry re-measurement and interactive device checks
remain pending. Synthetic regular-simulator coverage does not establish
native system-bar geometry or spoken VoiceOver behavior.

## Backlog reconciliation

The 13 preserved Duo records are tracked in Linear.
[STU-318](https://linear.app/kve/issue/STU-318) owns adoption and its
six implementation sub-issues. This slice relates to
[STU-318](https://linear.app/kve/issue/STU-318). Unmerged implementation
remains In Review.

Remaining Backlog work: native Duo release validation
([STU-316](https://linear.app/kve/issue/STU-316)), intermittent
Locations settling diagnostics
([STU-317](https://linear.app/kve/issue/STU-317)), and tall Year
accessibility annotations
([STU-325](https://linear.app/kve/issue/STU-325)). Existing
[STU-114](https://linear.app/kve/issue/STU-114) and
[STU-252](https://linear.app/kve/issue/STU-252) now distinguish the
partial fixes from their remaining requirements; both retain their
priorities, labels, and open state.
@kyleve
kyleve force-pushed the codex/duo-navigation-foundation branch 2 times, most recently from b55080d to 6695ad8 Compare November 11, 2026 17:39
import SwiftUI

/// Named synthetic configurations from SnapshotKit's measured Duo profiles.
enum AdaptiveHistoryPose: String, CaseIterable, Identifiable {

@kyleve kyleve Nov 11, 2026 •

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is only for testing, maybe we should move it into SnapshotKit itself?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Posted by an AI agent on kve's behalf.

Moved the seven synthetic configurations into SnapshotConfiguration.Pose in cb30be5. They reuse the existing frames and traits and carry typed reserved-region geometry plus activation state. WhereUI keeps the localized lab labels and its geometry adapter. Frame names, dimensions, snapshot IDs, and measurement provenance are unchanged; native 27.2 re-measurement remains pending.

}
}

var regions: [AdaptiveHistoryReservedRegion] {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar question; is this duplicated with anything in SnapshotKit?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Posted by an AI agent on kve's behalf.

Removed the separate pose geometry definitions in cb30be5. SnapshotKit now owns each synthetic pose and its reserved regions; WhereUI adapts those values to the production geometry API. AdaptiveHistoryGeometry remains in WhereUI because it interprets the container and system geometry for application layout.

import SwiftUI
import WhereCore

/// Retains the calendar's scroll anchor and requested planning presentation across remounts.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does "remount" here mean as the UI moves around as the device is folded, unfolded, rotated, etc?

I wonder if there's a more general way to support this for scrollable areas that have anchors for content; I gotta imagine this is more common than just calendars.

@kyleve kyleve Nov 11, 2026 •

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eg, imagining something like:

  • At the level of a stable container, place a ScrollPosition cache in the Environment
  • The positions are cached by the view type, and some identifier (eg, like the region)
  • Upon remounting the view, look up a scroll position value / write it up on scroll.

This would be a more general solution to the mount/remount problem than making a model for each view type.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Posted by an AI agent on kve's behalf.

Yes—replacement as a fold or layout branch changes. Implemented your stable-ancestor environment approach in cb30be5 and 9b386b5: .bScrollRestorationScope() owns a BScrollRestorationStore, and @BScrollPosition supplies a native binding keyed by view type and typed pane role. Region/year is a separate context token that replaces the current entry, so there is no history of old contexts. Without a scope it stays view-local. Mounted tests cover replacement, independent scopes/roles, delayed readiness, obsolete callbacks, and bottom alignment. #372 uses it for calendar detail, timeline, and Locations browsing.


/// Explicit selections connect history panes. Scroll observations never write this selection.
@MainActor @Observable
final class HistoryNavigationModel {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah similar comment to CalendarNavigationModel; this feels much more generalizable than we've made it here.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Posted by an AI agent on kve's behalf.

Agreed. cb30be5 removes scroll storage from the history coordinator. HistorySelectionModel now holds only the selected interval and explicit reveal requests, with distinct request IDs and independent consumption by each destination. Ordinary scrolling does not change the linked selection. The shared Broadway utility owns restoration; #372 integrates it and tests delayed data and repeated reveals.


/// Scroll observations retain a local anchor. Only explicit selection requests reveal a row.
@MainActor @Observable
final class TimelineNavigationModel {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah another one where IMO at least the scroll position stuff can be generalized.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Posted by an AI agent on kve's behalf.

Generalized in cb30be5 / 9b386b5 and integrated in #372 (cde4ff4). Timeline uses the same @BScrollPosition as calendar and Locations, with its own typed role and year context. The remaining timeline-specific code resolves explicit history selections against loaded domain items; it also retries when an existing stay extends without changing its ID.

/// Session-scoped presentation state. Resizing changes containers, never this owner or its
/// children.
@MainActor @Observable
final class SceneNavigationModel {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't 100% love this approach; what exactly is it solving? Why do we need this state when we didn't need it before; can the view itself not keep our navigation state like it did before?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Posted by an AI agent on kve's behalf.

Removed the aggregate SceneNavigationModel approach. cb30be5 supplies the reusable groundwork; cde4ff4 in #372 keeps paths, selection, search, sheets, and presentation helpers in stable owning views above adaptive branches. Models interpret data and access preferences. Editors keep their drafts locally, and an active pushed Locations stack remains mounted while resizing; tabletop waits until returning. Hosted guards cover pushed-controller identity, search, presented editors, unsaved drafts, and load continuity. Native keyboard/focus and physical folding validation remain pending.

@kyleve
kyleve removed this pull request from stack #379 November 11, 2026 18:38
@kyleve
kyleve added this pull request to stack #493 November 11, 2026 18:39
@kyleve
kyleve force-pushed the codex/duo-navigation-foundation branch from 6695ad8 to 2e1f0d2 Compare November 11, 2026 18:39
@kyleve
kyleve force-pushed the codex/duo-navigation-foundation branch from 2e1f0d2 to e02b3d9 Compare November 11, 2026 18:52
Base automatically changed from codex/duo-snapshot-foundation to main November 11, 2026 19:09
kyleve added a commit that referenced this pull request Nov 11, 2026
> _Posted by an AI agent on kve's behalf._

## Summary

Add explicit frame layout traits and recorded Duo profiles to the Xcode
27.2 baseline from main. Apply requested content clearance after UIKit
finalizes containment and native geometry.

This layer retains the existing Where navigation. Its image references
establish the new capture baseline before the adaptive app changes.
Calendar month sections, day rows, and Timeline rows measure eagerly so
full-content captures include the final rows; the selection and tabletop
changes remain in the app PR. Eager Timeline measurement removes blank
tails caused by lazy height estimates in a bottom-anchored scroll view.
Settings waits for native search chrome to settle before measuring its
full height. Existing scrolling onboarding and Devices fixtures use
full-content frames. Theme choices also fall back to a vertical layout
when narrow, and large-text Logged Days rows retain their dates, notes,
and labels.

## Screenshots

Developer Overlay, light iPhone full-content capture. Retaining the
scene restores uniform native glass without changing the menu's styling,
dimensions, or content.

| Before | After |
|---|---|
| <img alt="Detached-scene menu with bright bands and a hard material
cutoff" width="260"
src="https://media.githubusercontent.com/media/kyleve/Stuff/9e759fc8b0ac8db6569bfb03d49cdafd54c7ad2c/Where/WhereUI/SnapshotTests/__Snapshots__/DeveloperOverlaySnapshotTests/developerOverlay.MenuTopLeading_iPhone.png">
| <img alt="Scene-backed menu with uniform glass and complete content"
width="260"
src="https://media.githubusercontent.com/media/kyleve/Stuff/f05ca62400c415631e21c5e649ce655700d9f18a/Where/WhereUI/SnapshotTests/__Snapshots__/DeveloperOverlaySnapshotTests/developerOverlay.MenuTopLeading_iPhone.png">
|

Before: `9e759fc8`. After: `f05ca6240`; all six overlay captures
reproduced byte for byte.

## Primary APIs

- `SnapshotConfiguration.Frame.LayoutTraits` owns horizontal and
vertical size classes and interface idiom. Configuration-level layout
traits remain supported and take precedence over frame defaults. Phone
and iPad presets carry truthful traits without changing their filenames.
- `Frame.scenePolicy` controls capture scene retention without renaming
references. Both configured assertions and hosted PNG export honor it.
- `Frame.viewport` captures bounded shell composition. It must be paired
with full-content captures of scrolling panes.
- The four Duo profiles use measured native geometry. The module README
retains the measurement date, runtime/build, collection method, and
validation limits; the unused raw JSON dump is removed. `.narrowWindow`
covers a synthetic multitasking container.

```swift
let configuration = SnapshotConfiguration(device: .duoInnerPortrait)
configuration.applyTraits(to: host)
```

Previews and UIKit capture use the same mapping. Conversion from
configuration traits to frame traits uses exhaustive switches for both
size classes and idiom, so new enum cases require an explicit mapping.
Custom frames can supply `layoutTraits`; component frames inherit the
host's traits.

## Capture and tooling boundaries

All capture entry points acquire one lock before host discovery,
validation, and preparation. Fixed, regular-width phone captures retain
their scene so native bar geometry stays stable. Other synthetic
captures detach the marked window by default, preserving compact title
space and anchoring full-content search to expanded content. Capture and
measurement hosts reconcile their effective insets after containment and
layout, with another reconciliation after settling. The predicate uses
content-controller traits, not pixel width or simulator identity. Frames
can explicitly retain the scene when native chrome requires it.
Periscope and Developer Overlay use this option without changing their
configured traits, insets, or sizing. Developer Overlay's native glass
otherwise develops bright bands and a hard material cutoff. Retention
can expose physical reserved regions, so general presets keep the
automatic policy. Both paths restore scene and presentation state on
success or failure. A nil inset override retains native geometry.

Only the captured root overrides its safe-area getter, during final
pixel rendering. Native getters remain active during containment,
measurement, and settling so inset changes propagate to children. Root
pinning protects translated tiles. Overriding ancestors doubled
asymmetric insets after measurement rehosting. Annotation padding has
zero safe-area insets to avoid moving centered content. Tall tiled Year
annotations retain the pre-existing limitation listed below. Regressions
cover these boundaries, already-mounted inputs, compact and regular
title readability, native bar stability, tiling, and failure cleanup.
Full-content captures reset restored scroll offsets after readiness;
viewport captures preserve them.

Accessibility subjects stay mounted during readiness and settling.
Ordinary captures retain their controller parent during parsing; only
upstream tiling temporarily detaches it. Annotation wrappers grow only
after the final accessibility parse. Synthetic viewports recover native
navigation-bar metadata synchronously, restore window geometry before
parsing, and restore the original getters on every exit.

Pixel settling retains main’s normalized pixel comparison and retry
policy. Final reference-comparison tolerances remain unchanged. Hosted
resize, restoration, full-content, PNG export, and cancellation checks
cover the shared pipeline.

Snapshot shard discovery accepts literal multiline source arrays with
shared support files. Unsupported declarations fail without borrowing a
following target's sources. The existing shard assignments stay
unchanged.

## Compatibility and review focus

- The stack requires iOS 27.1. Development and CI use Xcode and
simulator 27.2. The native macOS deployment target is unchanged.
- Review the capture isolation and frame-trait API changes before the
references. The fix addresses stale inset compensation: requested
top/bottom values of 5/9 previously reached native children as 0/0.
- The app integration in #372 fixes a separate stale developer-overlay
inset delivery. Native accessibility capture corrections belong to this
foundation layer; later app layers own their navigation and search
behavior.
- The Duo profiles retain their recorded 27.1 measurements.
Re-measurement on the native 27.2 Duo runtime remains an explicit
release-validation item.
- Theme descriptions retain three lines of minimum space and grow for
large text. The shared picker no longer truncates the Flat description
at AX5.

## Testing

- At `42ab5b40e`, `./test --review --only SnapshotKitTests --only
WhereUISnapshotTests/DeveloperOverlaySnapshotTests` passed all 56
SnapshotKit tests and the Developer Overlay image suite (all six
captures matched byte for byte). Architecture and SwiftFormat passed.
Review fixes preserve the existing mappings and presets; no references
changed.
- Full gates on the current head `42ab5b40e`: [GitHub
Actions](https://github.com/kyleve/Stuff/actions/runs/43081928022) and
[CircleCI pipeline
2195](https://app.circleci.com/pipelines/circleci/HfkN3cJ2hk23YfG42g3oS1/E7hn5FALiSX67sknpk9SZu/2195).

- At `f05ca6240`, the full run passed 2,922 iOS unit tests and 28 host
macro tests. All 71 snapshot tests are covered by the full run plus
focused assertions after review; 679 affected WhereUI tests also passed
after the eager Timeline change.
- `./test --no-generate --review --timings --only WhereUITests` with the
seven affected snapshot suites verified the refreshed references. A
final `./test --no-generate --no-build --review --only
WhereUISnapshotTests/DeveloperOverlaySnapshotTests` repeated all six
overlay captures byte for byte. Architecture checks and SwiftFormat
passed.
- Full gates for the capture-repair baseline (before the
exhaustive-conversion refactor and documentation cleanup): [GitHub
Actions](https://github.com/kyleve/Stuff/actions/runs/43079785790) and
[CircleCI pipeline
2188](https://app.circleci.com/pipelines/circleci/HfkN3cJ2hk23YfG42g3oS1/E7hn5FALiSX67sknpk9SZu/2188).

The capture fixes and reference migration are verified at this PR's own
layer. A green stack tip cannot establish that an earlier PR's renderer
and references agree. Layer-specific Timeline, Settings, and Year
references retain the behavior present here rather than borrowing later
adaptive UI semantics.

- Independent source review covered capture containment, final
annotation sizing, native-bar getter restoration, Settings measurement,
and eager Timeline layout. Visual review covered the refreshed
references and preserved comparison-sheet metadata; comparison
tolerances are unchanged.
- Two tall Year accessibility images retain a pre-existing marker offset
and incomplete native-toolbar legends
([STU-325](https://linear.app/kve/issue/STU-325)).
- Native Duo 27.2 geometry re-measurement and interactive device checks
remain pending. Synthetic regular-simulator coverage does not establish
native system-bar geometry or spoken VoiceOver behavior.

## Stack

First active layer, based on main after
[#376](#376) merged. This supplies
the capture foundation for #378 and #372. Capture corrections and their
references land here; adaptive corrections and references land with the
app integration.

## Backlog reconciliation

The 13 preserved Duo records are tracked in Linear.
[STU-318](https://linear.app/kve/issue/STU-318) owns adoption and its
six implementation sub-issues. This slice relates to
[STU-319](https://linear.app/kve/issue/STU-319),
[STU-320](https://linear.app/kve/issue/STU-320),
[STU-327](https://linear.app/kve/issue/STU-327). Unmerged implementation
remains In Review.

Remaining Backlog work: native Duo release validation
([STU-316](https://linear.app/kve/issue/STU-316)), intermittent
Locations settling diagnostics
([STU-317](https://linear.app/kve/issue/STU-317)), and tall Year
accessibility annotations
([STU-325](https://linear.app/kve/issue/STU-325)). Existing
[STU-114](https://linear.app/kve/issue/STU-114) and
[STU-252](https://linear.app/kve/issue/STU-252) now distinguish the
partial fixes from their remaining requirements; both retain their
priorities, labels, and open state.
@kyleve
kyleve force-pushed the codex/duo-navigation-foundation branch from e02b3d9 to 19fb4a9 Compare November 11, 2026 19:09
Keep all nine UIKit size-class combinations in the pane-policy regression while mapping through the two-case Broadway API. Unspecified inputs must still select a single pane.

Validation: SwiftFormat and diff checks pass. Full unit integration runs at the rebased stack tip before publication; the root Broadway suites passed 141 tests.
@kyleve
kyleve force-pushed the codex/duo-navigation-foundation branch from 19fb4a9 to 68bd35e Compare November 11, 2026 19:09
Share synthetic pose geometry through SnapshotKit. Keep explicit history selection separate from view-owned presentation state. Add native hosted restoration, context replacement, delayed initialization, and scope isolation guards.
@kyleve kyleve changed the title refactor(WhereUI): add adaptive history state and layout policy refactor(UI): add scoped scroll restoration and Duo layout groundwork Nov 11, 2026
@Environment(\.bScrollRestorationStore) private var sharedStore
@State private var localStore = BScrollRestorationStore()
private let key: BScrollRestorationStore.Key
private let context: () -> AnyHashable

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirming we want this and not a more specific type?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Posted by an AI agent on kve's behalf.

Addressed in 4acf65c. BScrollPosition<ID, Context> retains the concrete Hashable context in its closure and mount. Type erasure now happens only when crossing into the heterogeneous restoration store. #372 migrates every consumer and test probe.


private var attachment: Attachment {
guard let resolvedAttachment else {
preconditionFailure("BScrollPosition must be read from a mounted view")

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be an assert so it doesn’t crash in prod

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Posted by an AI agent on kve's behalf.

Addressed in 4acf65c. Unmounted access uses assertionFailure in debug and returns a stable, isolated attachment in release. That fallback never resolves the environment store, so it cannot replace a mounted entry. Mounted/context-isolation tests pass; the release-only fallback was source-reviewed rather than executed by the debug suite.

extension SnapshotConfiguration {
/// Synthetic container geometry for adaptive previews, captures, and hosted tests.
/// These profiles do not change the simulator's native folding pose.
public enum Pose: String, CaseIterable, Identifiable, Sendable {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we name this duo pose?

Also, one case per case statement please?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Posted by an AI agent on kve's behalf.

Addressed in eb9dd29: renamed the type and matching files to SnapshotConfiguration.DuoPose, with one enum case per declaration. The frames, traits, reserved regions, activation state, and snapshot identifiers are unchanged.

import SwiftUI

extension SnapshotConfiguration.Pose {
var title: String {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any reason to not put these strings in snapshotkit too?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Posted by an AI agent on kve's behalf.

Addressed in eb9dd29. SnapshotKit now owns the seven localized pose labels and its resource catalog. WhereUI retains only the production geometry adapter; the label text is unchanged.


/// Connects explicit selections to displayed history data. It owns no scroll or presentation state.
@MainActor @Observable
final class PresenceSelectionModel {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we could simplify this by just giving each element we want to scroll to a more regular id? This feels very complicated to me

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Posted by an AI agent on kve's behalf.

Simplified in 7aa3146 and the #372 integration commit 4c0cd0a. Removed PresenceSelectionModel, per-pane consumption tracking, the timeline resolver, and the unused linked presentation. Timeline now owns its recorded/flight/planned row IDs. Locations owns a small CalendarMonthReveal value passed through @Binding; its request identity remains distinct from the month identity so selecting the same month again works after loading. Mounted tests cover delayed/repeated requests and browsing restoration without replaying a consumed request. Those tests caught a plain-binding observation gap during the refactor; registering the value with @Binding fixes it. Five annotation references in ca09946 remove the obsolete recorded-row Button trait; their screen pixels are unchanged, and planned-stay buttons remain.

}
}

/// Shares the native/injected coordinate mapping with custom chrome above the destinations.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What exactly is custom chrome? Why do we need it?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Posted by an AI agent on kve's behalf.

Clarified in fd1fd30, with the Settings simplification in #372 commit 0790712. The geometry reader is reused by the welcome and developer overlays to find clear space. It does not draw navigation bars or toolbars. Settings now uses native split navigation and stylesheet compact policy directly; Locations retains the pane resolver for its tabletop composition. Native Duo geometry validation remains pending.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant